package brickdestroyer.simulation;

import brickdestroyer.math.Vector2D;
import java.awt.Graphics;
import java.util.Stack;
import static brickdestroyer.simulation.Simulation.*;

/**
 * This class manages all balls in the game.
 * <p>It is responsible for updating all of them and
 * adding the abilities for new balls or the removal
 * of existing ones.
 */
public final class BallManager {

    /*
     * The starting position of the first ball when a game level
     * is started.
     */
    private static final Vector2D STARTING_BALL_POSITION =
            new Vector2D(PLAYABLE_AREA_CENTER_X, PLAYABLE_AREA_HEIGHT - 50.f);

    /*
     * Initial speed of the starting ball.
     */
    private static final Vector2D STARTING_BALL_SPEED = new Vector2D(0.0f, - 6.0f);

    /*
     * Timer representing the amounts of ticks after
     * which additional balls get removed.
     */
    private static final int BALL_TIMER_AMOUNT = 300;

    /*
     * Used for timing the ball removal sequence.
     */
    private int ballTimer = 0;

    /**
     * A stack of all the balls in the scene.
     */
    private final Stack<Ball> balls = new Stack<Ball>();

    private final Simulation simulation;
    
    public BallManager(Simulation simulation) {
        this.simulation = simulation;
        reset();
    }

    /**
     * This method is called to reset all balls to
     * their initial state when a level gets reset.
     * <p>This currently means that only one ball
     * gets left and is placed at the starting position
     * with an initial speed set to it.
     */
    public void reset() {
        balls.clear();
        Ball ball = new Ball();
        ball.setTo(STARTING_BALL_POSITION);
        ball.getSpeed().setTo(STARTING_BALL_SPEED);
        balls.push(ball);
    }

    /**
     * Determines whether the user has lost the game.
     * <p>Currently this is true only when one of the existing
     * balls has fallen bellow the screen.
     * @return <code>true</code> if the player has lost,
     * <code>false</code> otherwise.
     */
    public boolean gameIsOver() {
        for (Ball ball: balls) {
            if (ball.y > PLAYABLE_AREA_BOTTOM) {
                return true;
            }
        }
        return false;
    }

    /**
     * Adds a new ball to the game.
     * <p>This method returns a reference to the
     * new ball creating so that one can possition it
     * as necessary.
     * @return ball object being added.
     */
    public Ball addBall() {
        ballTimer = BALL_TIMER_AMOUNT;
        Ball ball = new Ball();
        balls.add(ball);
        return ball;
    }

    /**
     * This method removes the last added ball to
     * the game.
     */
    public void removeBall() {
        if (!balls.isEmpty()) {
            balls.pop();
        }
    }          

    /**
     * Updates the ball manager.
     * <p>This includes moving the balls and
     * checking for collisions. Also if there are
     * more then one balls in the game it checks if
     * it's time to remove one of them.
     */
    public void update() {
        updateBallTimer();
        /* This is to prevent concurrent modification */
        Stack<Ball> copyBalls = new Stack<Ball>();
        copyBalls.addAll(balls);
        for (Ball ball : copyBalls) {
            translateBall(ball);
        }
    }

    private void updateBallTimer() {
        if (balls.size() > 1) {
            ballTimer--;
            if (ballTimer <= 0) {
                removeBall();
                ballTimer = BALL_TIMER_AMOUNT;
            }
        }
    }

    private void translateBall(Ball ball) {
        PhysicsManager physicsManager = simulation.getPhysicsManager();
        ball.getSpeed().inc(physicsManager.getGravity());

        float displacementAmount = ball.getSmallestSize() * PhysicsManager.DISPLACEMENT_RATIO;
        float travelAmount = ball.getSpeed().getLength();

        Vector2D travelVector = new Vector2D(ball.getSpeed());
        travelVector.setLength(displacementAmount);

        while (travelAmount >= displacementAmount) {
            ball.inc(travelVector);
            if (checkCollisionAll(ball)) {
                travelVector.setTo(ball.getSpeed());
                travelVector.setLength(displacementAmount);
            }
            travelAmount -= displacementAmount;
        }
        travelVector.setLength(travelAmount);
        ball.inc(travelVector);
        checkCollisionAll(ball);

        ball.limitSpeed();
    }

    /*
     * Checks if a ball collides with any objects in the
     * game. In the current implementation ball to ball
     * collisions are not checked.
     */
    private boolean checkCollisionAll(Ball ball) {
        boolean foundCollision = false;
        foundCollision |= checkCollisionWalls(ball);
        foundCollision |= checkCollisionBricks(ball);
        SliderManager sliderManager = simulation.getSliderManager();
        if (checkCollisionPusher(ball)) {
            sliderManager.onHit(ball);
            foundCollision = true;
        }

        return foundCollision;
    }

    private boolean checkCollisionWalls(Ball ball) {
        boolean foundCollision = false;
        PhysicsManager physicsManager = simulation.getPhysicsManager();
        if (ball.getMinX() < PLAYABLE_AREA_LEFT) {
            ball.x = PLAYABLE_AREA_LEFT + ball.getRadius();
            Vector2D bounceVector = new Vector2D(-physicsManager.getBounce(), physicsManager.getSlide());
            ball.getSpeed().mul(bounceVector);
            foundCollision = true;
        }
        if (ball.getMaxX() > PLAYABLE_AREA_RIGHT) {
            ball.x = PLAYABLE_AREA_RIGHT - ball.getRadius();
            Vector2D bounceVector = new Vector2D(-physicsManager.getBounce(), physicsManager.getSlide());
            ball.getSpeed().mul(bounceVector);
            foundCollision = true;
        }
        if (ball.getMinY() < PLAYABLE_AREA_TOP) {
            ball.y = PLAYABLE_AREA_TOP + ball.getRadius();
            Vector2D bounceVector = new Vector2D(physicsManager.getSlide(), -physicsManager.getBounce());
            ball.getSpeed().mul(bounceVector);
            foundCollision = true;
        }
        return foundCollision;
    }

    private boolean checkCollisionBricks(Ball ball) {
        final int deltaCount = 8;
        final int deltaX[] = {  0, 0, - 1, 1, - 1,   1, - 1, 1};
        final int deltaY[] = {- 1, 1,   0, 0, - 1, - 1,   1, 1};

        BrickManager brickManager = simulation.getBrickManager();
        int brickX = brickManager.positionToBrickIndexX(ball.x);
        int brickY = brickManager.positionToBrickIndexY(ball.y);

        boolean foundCollision = false;
        for (int i = 0; i < deltaCount; ++i) {
            Brick brick = brickManager.getBrick(brickX + deltaX[i], brickY + deltaY[i]);
            if (checkCollisionBrick(brick, ball)) {
                foundCollision = true;
            }
        }
        return foundCollision;
    }

    private boolean checkCollisionBrick(Brick brick, Ball ball) {
        if (brick == null || brick.isDestroyed()) {
            return false;
        }

        CollisionManager collisionManager = simulation.getCollisionManager();
        if (collisionManager.checkCollision(ball, brick)) {
            brick.onHit(ball);
            return true;
        } else {
            return false;
        }
    }
       
    private boolean checkCollisionPusher(Ball ball) {
        SliderManager sliderManager = simulation.getSliderManager();
        CollisionManager collisionManager = simulation.getCollisionManager();
        return collisionManager.checkCollision(ball, sliderManager.getSlider());
    }

    /**
     * This method displays all the balls.
     * @param g interface to the graphics object.
     */
    public void display(Graphics g) {
        for (Ball ball : balls) {
            ball.display(g);
        }
    }
    
}
